﻿using System;
using System.Collections;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;

namespace Mongoose.Windows.Controls
{
    /// <summary>
    /// Surface that will handle Pad objects
    /// </summary>
    [TemplatePart(Name = "PART_Thumb", Type = typeof(Thumb))]
    public class Surface : ListBox, IPadContainer
    {
        /// <summary>
        /// Initializes the <see cref="Surface"/> class.
        /// </summary>
        static Surface()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Surface), new FrameworkPropertyMetadata(typeof(Surface)));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Surface"/> class.
        /// </summary>
        public Surface()
        {
            Layout = new SurfaceWrapLayout(this);
            Loaded += OnLoaded;
        }

        public IPadContainerLayout Layout
        {
            get { return (IPadContainerLayout)GetValue(LayoutProperty); }
            set { SetValue(LayoutProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Layout.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LayoutProperty =
            DependencyProperty.Register("Layout", typeof(IPadContainerLayout), typeof(Surface), new PropertyMetadata(null));

        public Matrix MatrixTransform
        {
            get { return (Matrix)GetValue(MatrixTransformProperty); }
            set { SetValue(MatrixTransformProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MatrixTransform.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MatrixTransformProperty =
            DependencyProperty.Register("MatrixTransform", typeof(Matrix), typeof(Surface), new PropertyMetadata(Matrix.Identity));

        public double MinimumScale
        {
            get { return (double)GetValue(MinimumScaleProperty); }
            set { SetValue(MinimumScaleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MinimumScale.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MinimumScaleProperty =
            DependencyProperty.Register("MinimumScale", typeof(double), typeof(Surface), new PropertyMetadata(0.2D));

        public double MaximumScale
        {
            get { return (double)GetValue(MaximumScaleProperty); }
            set { SetValue(MaximumScaleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MaximumScale.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MaximumScaleProperty =
            DependencyProperty.Register("MaximumScale", typeof(double), typeof(Surface), new PropertyMetadata(3.8D));

        /// <summary>
        /// Handles the Loaded event of the Surface control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
        /// <exception cref="System.NotImplementedException"></exception>
        private static void OnLoaded(object sender, RoutedEventArgs e)
        {
            var surface = sender as Surface;
            if (surface == null)
            {
                throw new InvalidOperationException();
            }
            surface.ArrangePads();
        }

        /// <summary>
        /// Gets a value indicating whether this instance has dynamic layout.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance has dynamic layout; otherwise, <c>false</c>.
        /// </value>
        public bool HasDynamicLayout
        {
            get
            {
                return Layout != null && Layout.IsDynamic;
            }
        }

        /// <summary>
        /// Arranges the pads.
        /// </summary>
        public void ArrangePads()
        {
            var pads = Canvas.Children.OfType<Pad>();
            if (pads != null && Layout != null)
            {
                Layout.ArrangePads(pads);
            }
        }

        /// <summary>
        /// Removes the specified pad from the Pads collection.
        /// </summary>
        /// <param name="pad">The pad.</param>
        public virtual void RemovePad(Pad pad)
        {
            var padContent = pad.Content;

            if (ItemsSource != null)
            {
                // Data binding scenario
                var itemsSourceList = ItemsSource as IList;
                if (itemsSourceList != null)
                {
                    var dataContext = pad.DataContext;
                    if (dataContext != null && itemsSourceList.Contains(dataContext))
                    {
                        itemsSourceList.Remove(dataContext);
                    }
                }
            }
            else
            {
                if (Items.Contains(padContent))
                {
                    Items.Remove(padContent);
                }
            }

            if (HasDynamicLayout)
            {
                ArrangePads();
            }
        }

        /// <summary>
        /// Invoked when an unhandled <see cref="E:System.Windows.Input.Mouse.MouseWheel" /> attached event reaches an element in its route that is derived from this class. Implement this method to add class handling for this event.
        /// </summary>
        /// <param name="e">The <see cref="T:System.Windows.Input.MouseWheelEventArgs" /> that contains the event data.</param>
        /// <exception cref="System.InvalidOperationException"></exception>
        protected override void OnMouseWheel(MouseWheelEventArgs e)
        {
            if (Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                // Set new scale factor
                var scale = (double)(e.Delta) > 0 ? 1.1 : 1 / 1.1;
                var oldScaleFactor = MatrixTransform.M11;
                var matrixTransform = MatrixTransform;
                matrixTransform.Scale(scale, scale);
                var newScaleFactor = matrixTransform.M11;

                // Scale factor must be comprised between 0.2 and 3.8
                if (newScaleFactor > MinimumScale && newScaleFactor < MaximumScale)
                {
                    // compute translation based on mouse position
                    var mousePosition = Mouse.GetPosition(Canvas);
                    var invariant = new Point((mousePosition.X - MatrixTransform.OffsetX) / oldScaleFactor, (mousePosition.Y - MatrixTransform.OffsetY) / oldScaleFactor);
                    var transformedInvariant = new Point((invariant.X * newScaleFactor) + MatrixTransform.OffsetX, (invariant.Y * newScaleFactor) + MatrixTransform.OffsetY);
                    var offset = new Point(transformedInvariant.X - mousePosition.X, transformedInvariant.Y - mousePosition.Y);

                    matrixTransform.OffsetX = MatrixTransform.OffsetX - offset.X;
                    matrixTransform.OffsetY = MatrixTransform.OffsetY - offset.Y;

                    MatrixTransform = matrixTransform;
                }
            }
        }

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code or internal processes call <see cref="M:System.Windows.FrameworkElement.ApplyTemplate" />.
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (Thumb != null)
            {
                Thumb.DragStarted += OnDragStarted;
                Thumb.DragDelta += OnDragDelta;
            }
        }

        /// <summary>
        /// Creates or identifies the element used to display a specified item.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Windows.Controls.ListBoxItem" />.
        /// </returns>
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new Pad(this);
        }

        /// <summary>
        /// Prepares the specified element to display the specified item.
        /// </summary>
        /// <param name="element">Element used to display the specified item.</param>
        /// <param name="item">Specified item.</param>
        /// <exception cref="System.InvalidOperationException"></exception>
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            var pad = element as Pad;
            if (pad == null)
            {
                throw new InvalidOperationException();
            }
            pad.SetBinding(Pad.SurfaceMatrixTransformProperty, new Binding("MatrixTransform") { Mode = BindingMode.OneWay, Source = this });
        }

        /// <summary>
        /// Invoked when an unhandled <see cref="E:System.Windows.UIElement.PreviewMouseLeftButtonDown" /> routed event reaches an element in its route that is derived from this class. Implement this method to add class handling for this event.
        /// </summary>
        /// <param name="e">The <see cref="T:System.Windows.Input.MouseButtonEventArgs" /> that contains the event data. The event data reports that the left mouse button was pressed.</param>
        protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnPreviewMouseLeftButtonDown(e);

            if (Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                // change surface offset based on mouse position
                var padAncestor = (InputHitTest(Mouse.GetPosition(Canvas)) as Visual).SafeFindAncestor<Pad>();

                var matrixTransform = Matrix.Identity;
                var actualScaleFactor = 1.0D;

                var availableWidth = ActualWidth - 40; // keep 20px margin right and left
                var availableHeight = ActualHeight - 40; // keep 20px margin top and bottom

                var elementWidth = 0.0D;
                var elementHeight = 0.0D;
                var elementX = 0.0D;
                var elementY = 0.0D;

                if (padAncestor != null && !padAncestor.IsPinned)
                {
                    elementWidth = padAncestor.ActualWidth;
                    elementHeight = padAncestor.ActualHeight;
                    elementX = padAncestor.Position.X;
                    elementY = padAncestor.Position.Y;
                }
                else
                {
                    // Fit to page if we clicked on the surface itself
                    var boundRect = GetBoundRect();
                    elementWidth = boundRect.Width;
                    elementHeight = boundRect.Height;
                    elementX = boundRect.X;
                    elementY = boundRect.Y;
                }

                var scaleFactor = Math.Min(availableWidth / elementWidth, availableHeight / elementHeight);
                if (scaleFactor < 1)
                {
                    actualScaleFactor = scaleFactor;
                    matrixTransform.Scale(actualScaleFactor, actualScaleFactor);
                }

                matrixTransform.OffsetX = -elementX * actualScaleFactor - (elementWidth * actualScaleFactor - ActualWidth) / 2;
                matrixTransform.OffsetY = -elementY * actualScaleFactor - (elementHeight * actualScaleFactor - ActualHeight) / 2;

                if (double.IsNaN(matrixTransform.OffsetX))
                {
                    matrixTransform.OffsetX = 0.0D;
                }

                if (double.IsNaN(matrixTransform.OffsetY))
                {
                    matrixTransform.OffsetY = 0.0D;
                }

                // animate matrix change
                var matrixAnimation = new MatrixAnimation(MatrixTransform, matrixTransform, new Duration(new TimeSpan(0, 0, 0, 0, 200)));
                matrixAnimation.Completed += (sender, ev) => { MatrixTransform = matrixTransform; BeginAnimation(MatrixTransformProperty, null); };
                BeginAnimation(MatrixTransformProperty, matrixAnimation);

            }
        }

        /// <summary>
        /// Handles the DragStarted event of the Thumb control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="DragStartedEventArgs"/> instance containing the event data.</param>
        /// <exception cref="System.NotImplementedException"></exception>
        private static void OnDragStarted(object sender, DragStartedEventArgs e)
        {
            if (!Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                var thumb = sender as Thumb;
                if (thumb == null)
                {
                    throw new InvalidOperationException();
                }
                var surface = thumb.Tag as Surface;
                if (surface == null)
                {
                    throw new InvalidOperationException();
                }

                var transformMatrix = surface.MatrixTransform;
                surface.dragSource = new Point(transformMatrix.OffsetX, transformMatrix.OffsetY);
            }
        }

        /// <summary>
        /// Called on DragDelta event on the underlying Thumb object.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="DragDeltaEventArgs"/> instance containing the event data.</param>
        private static void OnDragDelta(object sender, DragDeltaEventArgs e)
        {
            if (!Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                var thumb = sender as Thumb;
                if (thumb == null)
                {
                    throw new InvalidOperationException();
                }
                var surface = thumb.Tag as Surface;
                if (surface == null)
                {
                    throw new InvalidOperationException();
                }

                var transformMatrix = surface.MatrixTransform;
                transformMatrix.OffsetX = surface.dragSource.X + e.HorizontalChange;
                transformMatrix.OffsetY = surface.dragSource.Y + e.VerticalChange;
                surface.MatrixTransform = transformMatrix;
            }
        }

        /// <summary>
        /// Gets the thumb used to manage surface Move operations.
        /// </summary>
        /// <value>
        /// The thumb.
        /// </value>
        internal Thumb Thumb
        {
            get
            {
                if (thumb == null)
                {
                    thumb = GetTemplateChild("PART_Thumb") as Thumb;
                    thumb.Tag = this;
                }
                return thumb;
            }
        }

        /// <summary>
        /// Gets the surface canvas.
        /// </summary>
        /// <value>
        /// The canvas.
        /// </value>
        internal Canvas Canvas
        {
            get 
            {
                if (canvas == null)
                {
                    canvas = this.SafeFindDescendant<Canvas>(c => c.Name == "SurfaceCanvas");
                }
                return canvas;
            }
        }


        /// <summary>
        /// Gets the bound rect.
        /// </summary>
        /// <returns></returns>
        private Rect GetBoundRect()
        {
            var result = new Rect();

            var minLeft = double.NaN;
            var minTop = double.NaN;
            var maxRight = double.NaN;
            var maxBottom = double.NaN;

            foreach (Pad pad in Canvas.Children)
            {
                if (pad.IsPinned)
                {
                    continue;
                }
                minLeft = double.IsNaN(minLeft) ? pad.Position.X : Math.Min(minLeft, pad.Position.X);
                minTop = double.IsNaN(minTop) ? pad.Position.Y : Math.Min(minTop, pad.Position.Y);
                maxRight = double.IsNaN(maxRight) ? pad.Position.X + pad.ActualWidth : Math.Max(maxRight, pad.Position.X + pad.ActualWidth);
                maxBottom = double.IsNaN(maxBottom) ? pad.Position.Y + pad.ActualHeight : Math.Max(maxBottom, pad.Position.Y + pad.ActualHeight);
            }

            result = new Rect(new Point(minLeft, minTop), new Point(maxRight, maxBottom));

            return result;
        }

        #region Private fields
        private Thumb thumb;
        private Canvas canvas;
        private Point dragSource;
        #endregion
    }
}
